#include "dmac.h"

void DMAC_Init(void)
{
  DMAC->Configuration = 0;
  for (int i = 0; i < DMAC_CHANNELS; i++) {
    DMAC->Channels[i].Configuration = 0;
  }
  DMAC->IntErrorClear = 0xFFFFFFFF;
  DMAC->IntTCClear    = 0xFFFFFFFF;
  DMAC->Configuration = DMAC_M1_LITTLE_ENDIAN | DMAC_M2_LITTLE_ENDIAN | ENABLE;
}

void DMAC_HaltChannel(DMAC_ChannelNumTypeDef channel)
{
  // To avoid losing data in FIFO, set the halt bit so that any subsequent DMA request is ignored. Then disable the
  // channel after it is no longer active
  DMAC->Channels[channel].Configuration |= (1 << DMAC_CHANNEL_HALT_OFFSET);
  while (DMAC->Channels[channel].Configuration & (1 << DMAC_CHANNEL_ACTIVE_OFFSET));
  DMAC_DisableChannel(channel);
}

uint32_t DMAC_MakeControl(DMAC_AddrIncTypeDef srcIncr, DMAC_AddrIncTypeDef dstIncr,
                          DMAC_WidthTypeDef srcWidth, DMAC_WidthTypeDef dstWidth,
                          DMAC_BurstTypeDef srcBurst, DMAC_BurstTypeDef dstBurst,
                          uint32_t transferSize, EnableTypeDef enableIntTC)
{
  return (transferSize << DMAC_TRANSFER_SIZE_OFFSET |
          srcBurst     << DMAC_SRC_BURST_OFFSET     |
          dstBurst     << DMAC_DST_BURST_OFFSET     |
          srcWidth     << DMAC_SRC_WIDTH_OFFSET     |
          dstWidth     << DMAC_DST_WIDTH_OFFSET     |
          DMAC_AHB_M1  << DMAC_SRC_MASTER_OFFSET    |
          DMAC_AHB_M2  << DMAC_DST_MASTER_OFFSET    |
          srcIncr      << DMAC_SRC_ADDR_INCR_OFFSET |
          dstIncr      << DMAC_DST_ADDR_INCR_OFFSET |
          enableIntTC  << DMAC_TC_INT_ENABLE_OFFSET);
}

uint32_t DMAC_MakeConfiguration(DMAC_FlowControlTypeDef transferType, uint32_t srcPeripheral, uint32_t dstPeripheral)
{
  return (ENABLE        << DMAC_ENABLE_OFFSET         |
          srcPeripheral << DMAC_SRC_PERIPHERAL_OFFSET |
          dstPeripheral << DMAC_DST_PERIPHERAL_OFFSET |
          transferType  << DMAC_FLOW_CONTROL_OFFSET   |
          ENABLE        << DMAC_INT_ERROR_MASK_OFFSET |
          ENABLE        << DMAC_INT_TC_MASK_OFFSET);
}

// Config function to initiate 1 DMA tranfer. Transfer size cannot exceed DMAC_MAX_TRANSFER_SIZE
void DMAC_Config(DMAC_ChannelNumTypeDef channel, uint32_t srcAddr,uint32_t dstAddr,
                 DMAC_AddrIncTypeDef srcIncr, DMAC_AddrIncTypeDef dstIncr,
                 DMAC_WidthTypeDef srcWidth, DMAC_WidthTypeDef dstWidth,
                 DMAC_BurstTypeDef srcBurst, DMAC_BurstTypeDef dstBurst,
                 uint32_t transferSize, DMAC_FlowControlTypeDef transferType,
                 uint32_t srcPeripheral, uint32_t dstPeripheral)
{
  DMAC_ClearChannelIntError(channel);
  DMAC_ClearChannelIntTC(channel);

  DMAC_SetSrcAddress   (channel, srcAddr);
  DMAC_SetDstAddress   (channel, dstAddr);
  DMAC_SetLLI          (channel, 0);
  DMAC_SetControl      (channel, DMAC_MakeControl(srcIncr, dstIncr, srcWidth, dstWidth,srcBurst, dstBurst, transferSize, ENABLE));
  DMAC_SetConfiguration(channel, DMAC_MakeConfiguration(transferType, srcPeripheral, dstPeripheral));
}

// Config function to initiate DMA from the first link list item
void DMAC_ConfigLLI(DMAC_ChannelNumTypeDef channel, DMAC_FlowControlTypeDef transferType,
                    uint32_t srcPeripheral, uint32_t dstPeripheral, uint32_t lli)
{
  DMAC_ClearChannelIntError(channel);
  DMAC_ClearChannelIntTC(channel);

  DMAC_LLI_TypeDef *ptr = (DMAC_LLI_TypeDef *)lli;

  DMAC_SetSrcAddress   (channel, ptr->SrcAddr);
  DMAC_SetDstAddress   (channel, ptr->DstAddr);
  DMAC_SetLLI          (channel, ptr->LLI);
  DMAC_SetControl      (channel, ptr->Control);
  DMAC_SetConfiguration(channel, DMAC_MakeConfiguration(transferType, srcPeripheral, dstPeripheral));
}

// This function can handle any transfer size. If the size is greater than DMAC_MAX_TRANSFER_SIZE, link list items will 
// be automatically allocated and returned. The caller should deallocate the memory after all items are finished. 
// Alternatively, an LLI array can be given such that no memory allocation is necessary. In this case the caller must 
// make sure that the LLI array is valid in memory throughout the transfer.
DMAC_LLI_TypeDef *DMAC_AutoTransfer(DMAC_ChannelNumTypeDef channel, uint32_t srcAddr,uint32_t dstAddr,
                                    DMAC_AddrIncTypeDef srcIncr, DMAC_AddrIncTypeDef dstIncr,
                                    DMAC_WidthTypeDef srcWidth, DMAC_WidthTypeDef dstWidth,
                                    DMAC_BurstTypeDef srcBurst, DMAC_BurstTypeDef dstBurst,
                                    uint32_t transferSize, DMAC_FlowControlTypeDef transferType,
                                    uint32_t srcPeripheral, uint32_t dstPeripheral,
                                    EnableTypeDef enableIntTC, DMAC_LLI_TypeDef *lli_arr)
{
  DMAC_LLI_TypeDef  lli_val; // The first lli
  DMAC_LLI_TypeDef *lli_ptr = lli_arr;

  const int LLI_COUNT = (transferSize + DMAC_MAX_TRANSFER_SIZE - 1) / DMAC_MAX_TRANSFER_SIZE;
  if (LLI_COUNT > 1 && !lli_ptr) {
    lli_ptr = AlignedMalloc(4, (LLI_COUNT - 1) * sizeof(DMAC_LLI_TypeDef));
  }
  
  for (int i = 0; i < LLI_COUNT; ++i) {
    DMAC_LLI_TypeDef *lli = (i == 0) ? &lli_val : lli_ptr + (i - 1);
    lli->SrcAddr = srcAddr + (srcIncr == DMAC_ADDR_INCR_ON ? i * DMAC_MAX_TRANSFER_SIZE * (1 << srcWidth) : 0);
    lli->DstAddr = dstAddr + (dstIncr == DMAC_ADDR_INCR_ON ? i * DMAC_MAX_TRANSFER_SIZE * (1 << srcWidth) : 0);
    lli->LLI     = (i < LLI_COUNT - 1) ? (uint32_t)(lli_ptr + i) : 0;
    lli->Control = DMAC_MakeControl(srcIncr, dstIncr, srcWidth, dstWidth, srcBurst, dstBurst,
                                      (i < LLI_COUNT - 1) ? DMAC_MAX_TRANSFER_SIZE : transferSize - (LLI_COUNT - 1) * DMAC_MAX_TRANSFER_SIZE,
                                      (i < LLI_COUNT - 1) ?  DISABLE : enableIntTC); // Interrupt only for the last lli
  }
  DMAC_ConfigLLI(channel, transferType, srcPeripheral, dstPeripheral, (uint32_t)(&lli_val));
  return lli_ptr;
}

void DMAC_WaitedTransfer(DMAC_ChannelNumTypeDef channel, uint32_t srcAddr,uint32_t dstAddr,
                         DMAC_AddrIncTypeDef srcIncr, DMAC_AddrIncTypeDef dstIncr,
                         DMAC_WidthTypeDef srcWidth, DMAC_WidthTypeDef dstWidth,
                         DMAC_BurstTypeDef srcBurst, DMAC_BurstTypeDef dstBurst,
                         uint32_t transferSize, DMAC_FlowControlTypeDef transferType,
                         uint32_t srcPeripheral, uint32_t dstPeripheral)
{
  DMAC_LLI_TypeDef lli[(transferSize + DMAC_MAX_TRANSFER_SIZE - 1) / DMAC_MAX_TRANSFER_SIZE];
  DMAC_AutoTransfer(channel, srcAddr, dstAddr, srcIncr, dstIncr, srcWidth, dstWidth,
                    srcBurst, dstBurst, transferSize, transferType, srcPeripheral, dstPeripheral, DISABLE, lli);
  DMAC_WaitChannel(channel); // Must wait for DMA to finish before freeing the link list items
}
